今年我延續了 2025 iThome 鐵人賽的 Learn HTTP With JS,將會繼續撰寫第 31 ~ 60 篇關於 HTTP 的文章
不管是寫前端的 JavaScript
addEventListener("click", () => {});
還是後端的 Node.js
import { createServer } from "http";
const httpServer = createServer().listen(5000);
httpServer.on("request", (req, res) => {
req.on("data", (chunk) => {});
req.on("end", () => {});
});
都離不開 Event-driven architecture (EDA) 這個概念
尤其是 "有用過 Node.js 原生 http 模組寫 client / server" 的人類們,應該都看過
EventEmitter
stream.Readable
stream.Writable
net.Socket
等等名詞,還有一堆很抽象的方法
import { createServer } from "http";
const httpServer = createServer().listen(5000);
httpServer.on("request", (req, res) => {
res.write(); // write 跟 end 差在哪 ❓
res.end();
res.destroy(); // 啥時要呼叫 destroy ❓
res.setHeader(); // 所以到底要用哪個方法添加 header 啦 ❓
res.appendHeader();
res.flushHeaders();
res.writeHead();
res.pipe(); // pipe 又是什麼鬼 ❓
res.cork(); // 這又是什麼妖魔鬼怪 ❓
res.uncork();
});
這問題困擾了我很久,畢竟前端工程師用 JavaScript 要發起 HTTP request 的話,都是用高階、封裝好的 API
fetch("https://your-api-server.com", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ hello: "world" }),
})
.then((res) => res.json())
.then((res) => console.log(res));
如果想搞懂 Node.js http 模組的話,你必須得知道這一整條路的關聯
所以,接下來我會規劃數篇文章,一路涵蓋 EventEmitter、stream、net.Socket 跟 http
https://nodejs.org/api/events.html#events
on 註冊事件emit 觸發事件import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on("customEvent", () => {
console.log("customEvent occurred!");
});
myEmitter.emit("customEvent");
https://nodejs.org/api/events.html#passing-arguments-and-this-to-listeners
import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on(
"customEvent",
// ✅ 使用 function 而不是箭頭函數,才能正確綁定 this
function (this: EventEmitter, a: string, b: string) {
console.log(a, b, this === myEmitter); // a b true
},
);
myEmitter.emit("customEvent", "a", "b");
https://nodejs.org/api/events.html#asynchronous-vs-synchronous
import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on("customEvent", function () {
console.log("first customEvent listener");
});
myEmitter.on("customEvent", function () {
console.log("second customEvent listener");
});
myEmitter.emit("customEvent");
// first customEvent listener
// second customEvent listener
https://nodejs.org/api/events.html#handling-events-only-once
import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// ✅ 使用 once 讓事件只觸發一次
myEmitter.once("customEvent", function () {
console.log("customEvent triggered");
});
myEmitter.emit("customEvent"); // customEvent triggered
myEmitter.emit("customEvent"); // 忽略
https://nodejs.org/api/events.html#error-events
若沒有註冊 on("error"),則 emit("error") 會讓 Node.js process exit
❌ 錯誤作法
import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.emit("error", new Error("oops...")); // ❌ 沒有註冊 on("error")
✅ 正確做法
import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on("error", (err) => console.log(err)); // ✅ 正確註冊 on("error")
myEmitter.emit("error", new Error("oops..."));
https://nodejs.org/api/events.html#error-events
on(errorMonitor),再觸發 on("error")
on("error"),才不會讓 Node.js process exitimport EventEmitter, { errorMonitor } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on(errorMonitor, (err) => {
console.error("implement your custom error logger logic");
});
myEmitter.on("error", (err) => console.log(err));
myEmitter.emit("error", new Error("oops..."));
https://nodejs.org/api/events.html#capture-rejections-of-promises
在 on("event") 使用 async function as listener 的話,需要特別注意 unhandled promise rejection
❌ 錯誤作法
import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on("hello", async () => {
throw new Error("promise reject");
});
myEmitter.on("error", (err) => console.log("error occured")); // ❌ 無法捕捉 promise rejection
myEmitter.emit("hello");
✅ 正確做法
import { EventEmitter } from "events";
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter({ captureRejections: true }); // ✅ 加上 captureRejections
myEmitter.on("hello", async () => {
throw new Error("promise reject");
});
myEmitter.on("error", (err) => console.log("error occured"));
myEmitter.emit("hello");
其實 EventEmitter 的概念蠻好懂的,可以把資料結構想成陣列
const myDailyEvents = {
morning: [function brushMyTeeth() {}, function haveBreakfast() {}],
afternoon: [function haveLunch() {}, function takeANap() {}],
night: [function haveDinner() {}, function playVideoGames() {}],
};
執行順序就是按照陣列的順序
當你觸發 myDailyEventsEmitter.emit("morning"),就會先刷牙 (brushMyTeeth),再吃早餐 (haveBreakfast)
如果想要在刷牙前摺棉被呢?只要用 prependListener 這個方法就好
myDailyEventsEmitter.prependListener("morning", function foldTheBlanket() {});
這個概念就是 array 的操作,插頭還是插尾
myDailyEventsEmitter.on("morning", function goToWork() {});
// 插尾,等同於 myDailyEvents.morning.push(function goToWork () {})
myDailyEventsEmitter.prependListener("morning", function foldTheBlanket() {});
// 插頭,等同於 myDailyEvents.morning.unshift(function foldTheBlanket () {})
如果希望摺棉被只要摺一次呢?只要用 prependOnceListener 這個方法就好
myDailyEventsEmitter.prependOnceListener(
"morning",
function foldTheBlanket() {},
);
有了這個概念,再來看 EventEmitter 的 methods,就會發現其實就是圍繞 event 跟 listener 的 CRUD
平常應該用不到,看看就好
基本上就是 event 跟 listener 的 CRUD,有需要再查官方文件
eventNames
getMaxListeners
listenerCount
listeners
off
removeAllListeners
removeListener
setMaxListeners
rawListeners
getEventListeners
這些我覺得對於學習 Node.js http 模組沒有立即的幫助,且後面三者都是 Web API 移植到 Node.js,使用情境我覺得不高~
EventEmitter 是 Node.js Event-driven architecture 的基礎,但它只負責「註冊事件」和「觸發事件」
當你看到 Node.js http 模組相關的程式碼:
req.on("data", (chunk) => {}); // data 事件什麼時候觸發 ❓
req.on("end", () => {}); // end 事件什麼時候觸發 ❓
req.pipe(res); // pipe 是什麼 ❓
這些問題的答案都在 stream 裡
EventEmitter 只是提供了「事件機制」,而 stream 才定義了「什麼時候該觸發這些事件」
下一篇我們來看 stream 如何基於 EventEmitter 建立「資料流」的概念